Skip to main content

17 垃圾回收:V8的垃圾回收器

V8 为了更高效地回收垃圾,引入了两个垃圾回收器,分别针对不同的场景。

垃圾数据是怎么产生的

数据会被存放到栈和堆中,通常的方式是在内存中创建一块空间,使用这块空间,在不需要的时候回收这块空间。

window.test = new Object();
window.test.a = new Uint16Array(100);

为 window 对象添加一个 test 属性,并在堆中创建了一个空对象,将该对象的地址指向 window.test 属性。随后又创建一个大小为 100 的数组,并将属性地址指向了 test.a 的属性值。

window.test.a = new Object();

a 属性之前指向堆中数组对象,现在已经指向了另外一个空对象,此时堆中的数组对象就成为了垃圾数据,因为无法从一个根对象遍历到这个 Array 对象。V8 虚拟机中的垃圾回收器就会帮忙自动清理该数据。

垃圾回收算法

通过 GC Root 标记空间中活动对象和非活动对象

V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:

  • 通过 GC Root 遍历到的对象,就认为该对象是可访问的(reachable),这些对象应该在内存中保留,称可访问的对象为活动对象;
  • 通过 GC Roots 没有遍历到的对象,则是不可访问的(unreachable),这些不可访问的对象就可能被回收,称不可访问的对象为非活动对象。

在浏览器环境中的 GC Root:

  • 全局的 window 对象(位于每个 iframe 中);
  • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成;
  • 存放栈上变量。

回收非活动对象所占据的内存

在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。

做内存整理

频繁回收对象后,内存中会存在大量不连续空间,称为内存碎片。如果需要分配较大的连续内存时,有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。有些垃圾回收器不会产生内存碎片。


受到代际假说(The Generational Hypothesis)的影响,V8 采用了两个垃圾回收器,主垃圾回收器 -Major GC副垃圾回收器 -Minor GC (Scavenger)

代际假说的两个特点:

  • 大部分对象在内存中存活的时间很短(比如函数内部声明的变量,块级作用域中的变量,当函数或者代码块执行结束时,作用域中定义的变量就会被销毁)。这一类对象一经分配内存,很快就变得不可访问;
  • 小部分不死的对象,会活得很久(比如全局的 window、DOM、Web API 等对象)。

只使用一个垃圾回收器,优化大多数新对象时,很难优化到老对象,因此需要权衡各种场景,根据对象生存周期的不同,而使用不同的算法。因此 V8 把堆分为新生代和老生代两个区域,新生代中存放生存时间短的对象,老生代中存放生存时间久的对象

新生代通常只支持 1 ~ 8M 的容量,老生代支持的容量大很多。V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。

  • 副垃圾回收器 -Minor GC (Scavenger),主要负责新生代的垃圾回收。
  • 主垃圾回收器 -Major GC,主要负责老生代的垃圾回收。

副垃圾回收器

副垃圾回收器主要负责新生代的垃圾回收。大多数小的对象都会被分配到新生代,这个区域虽然不大,但是垃圾回收比较频繁。

新生代中的垃圾数据用 Scavenge 算法来处理,把新生代空间对半划分为两个区域,一半是对象区域 (from-space),一半是空闲区域 (to-space):

新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。

垃圾回收时,先对对象区域中的垃圾做标记,然后把这些存活的对象复制到空闲区域中,同时把这些对象有序地排列起来,复制过程相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。

完成复制后,原来的对象区域变成空闲区域,原来的空闲区域变成对象区域。就完成了垃圾对象的回收操作,同时让新生代中的这两块区域无限重复使用下去。

副垃圾回收器会采用对象晋升策略,移动那些经过两次垃圾回收依然还存活的对象到老生代中。

主垃圾回收器

主垃圾回收器主要负责老生代中的垃圾回收。除了新生代中晋升的对象,一些大的对象会直接被分配到老生代里。老生代中的对象的两个特点:

  • 对象占用空间大;
  • 对象存活时间长。

主垃圾回收器采用**标记 - 清除(Mark-Sweep)**的算法进行垃圾回收:

标记过程阶段

从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。

垃圾的清除过程

主垃圾回收器会直接将标记为垃圾的数据清理掉。

对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。碎片过多会导致大对象无法分配到足够的连续内存,于是又引入了另外一种算法——标记 - 整理(Mark-Compact)

不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉这一端之外的内存。